wildcodeschool-logo.png

Importation des packages

In [1]:
import pandas as pd
import plotly.express as px
import plotly.graph_objs as go
from plotly.subplots import make_subplots
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.linear_model import LinearRegression
from sklearn.neighbors import KNeighborsRegressor
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestRegressor
import warnings
warnings.filterwarnings('ignore')

Importation du dataset

In [2]:
ventes = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/history.csv")
bordeaux = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/bordeaux2019.csv",
    skiprows=3)
lille = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/lille2019.csv",
    skiprows=3)
lyon = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/lyon2019.csv",
    skiprows=3)
marseille = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/marseille2019.csv",
    skiprows=3)
prev_meteo_fin_juin = pd.read_csv(
    "https://raw.githubusercontent.com/murpi/wilddata/master/test/forecast.csv"
)

Description du dataset

  • ventes: nombre des ventes par item classé par date durant l'année 2019;
  • bordeaux: historique météorologique de 2019 pour la ville de Bordeaux;
  • lille: historique météorologique de 2019 pour la ville de Lille;
  • lyon: historique météorologique de 2019 pour la ville de Lyon;
  • marseille: historique météorologique de 2019 pour la ville de Marseille;
  • prev_meteo_fin_juin: prévision météorologique du 21 juin 2021 au 27 juin 2021 inclus.

Analyse descriptive exploratoire de ventes

Nous analyserons les données de vente du DataFrame ventes en vérifiant tout d'abord s'il y a des données manquantes et si le Dtype pour chaque variable est correct. Nous observerons par la suite comment les ventes des différents produits varient au cours de l'année.

Vérification des données

Méthode info()

On vérifie avec la méthode info():

  • la forme du DataFrame avec son nombre d'entrées et de colonnes;
  • la présence de valeurs null;
  • le bon Dtype pour chaque variable.
In [3]:
ventes.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 520 entries, 0 to 519
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   DATE    520 non-null    object
 1   ITEM    520 non-null    object
 2   SALES   520 non-null    int64 
dtypes: int64(1), object(2)
memory usage: 12.3+ KB

Résultat: la variable DATE affiche un Dtype object, il convient de transtyper cette variable en datetime. Sur les 520 entrées aucunes données nous sont retournées nulles.

Méthode duplicated()

La méthode .duplicated() nous retourne pour toutes les lignes si des doublons sont présents par des valeurs booléennes: True ou False. Si on ajoute la méthode .unique() nous agrégerons la réponse en vérifant si les 2 valeurs nous sont ou non retournées.

Si:

  • False et True: un ou des doublons sont présents;
  • False: aucun doublon n'est présent.
In [4]:
print(ventes.duplicated().unique())
[False]

Transtypage et modification du format de la variable DATE

Pour faciliter notre analyse nous utiliserons la fonction pd.to_datetime() pour transtyper la variable en datetime.

In [5]:
# Transtypage object en datetime
ventes["DATE"] = pd.to_datetime(ventes["DATE"], format="%d-%m-%Y")

Saisonalité des ventes

Pour observer la saisonnalité des ventes, nous allons classer les ventes par mois et par produits vendus grâce à la fonction plot_saisonnalite_ventes().

Dans un premier temps, pour faciliter notre analyse, nous utiliserons la fonction pd.to_datetime() pour transtyper la variable en datetime. On utilisera la méthode dt.strftime() pour formater la colonne de dates en mois avec l'argument "%m". Une fois notre colonne MOIS créée, nous l'utiliserons à la place de DATE et regrouperons par mois les ventes totales (SALES) par groupe de produits (ITEM).

Afin de mieux observer et analyser l'évolution des ventes selon les produits, nous utiliserons la libraire Plotly pour créer un graphique à barres ou un graphique utilisant des lignes. Ce dernier graphique nous permettra de vérifier la corrélation entre les ventes et les données météoroliques.

Création de la fonction saisonnalite_ventes()

In [6]:
def plot_saisonnalite_ventes(df, viz="bar"):
    """
    Cette fonction prend en entrée un dataframe "df" contenant les données
    de ventes et un argument optionnel "viz" qui permet de choisir entre
    un graphique à barres ou un graphique avec des lignes pour visualiser
    la saisonnalité des ventes.
    Par défaut, la visualisation est réalisée avec un graphique à barres.
    
    Dependencies:
    - pandas
    - plotly
    
    Parameters:
    df (DataFrame): Dataframe contenant les données de ventes
    viz (str): Type de visualisation souhaitée. "bar" pour un graphique à
    barres ou "line" pour un graphique avec des lignes.
    
    Returns:
    Graphique de la saisonnalité des ventes pour chaque produit, soit sous
    forme de graphique à barres ou de graphique avec des lignes.
    """
    # On crée une nouvelle colonne "MOIS" pour regrouper les ventes par mois
    df["MOIS"] = df["DATE"].dt.strftime("%m")
    # On agrège les ventes par mois pour chaque produit
    df = df.groupby(["MOIS", "ITEM"], as_index=False)["SALES"].sum()

    # On crée le graphique de la saisonnalité des ventes
    # On retourne un graphique à barres par défaut...
    if viz == "bar":
        fig = px.bar(df,
                     x="MOIS",
                     y="SALES",
                     color="ITEM",
                     barmode="group",
                     title="<b>Saisonalité des ventes</b>",
                     text_auto=True,
                     height=600,
                     width=980)
        fig.update_layout(bargap=0.2,
                          title_x=0.5,
                          yaxis_title="Chiffre d'affaires (en €)")

    # ... ou un graphique à lignes si viz="line"
    elif viz == "line":
        fig = px.line(df,
                      x="MOIS",
                      y="SALES",
                      color="ITEM",
                      title="<b>Saisonalité des ventes</b>",
                      height=600,
                      width=980)
        # On ajoute un marqueur par mois sur chaque ligne
        for trace in fig.data:
            trace.update(mode="lines+markers")
        fig.update_layout(title_x=0.5, yaxis_title="Chiffre d'affaires (en €)")

    # Un message d'erreur si viz != de "bar" ou "line"
    else:
        print("Erreur: l'argument viz doit être 'bar' ou 'line'.")
        return

    fig.show()

Visualisation avec graphique à barres

In [7]:
plot_saisonnalite_ventes(ventes)

Visualisation avec graphique à lignes

In [8]:
plot_saisonnalite_ventes(ventes, viz="line")

Calcul du chiffre d'affaire par produit

# Calcul du montant total des ventes par produit sur l'année 2019 ventes_mois_par_item.groupby("ITEM")["SALES"].sum()

Analyse des ventes

  • Sur l'ensemble de l'année, les produits B ont rapporté plus que les A avec 28 883€ contre 22 185€;
  • Un net écart s'observe entre les mois de mai et octobre (tous deux inclus) où le CA des produits B est plus fort. A l'inverse le CA des produits A est plus fort entre novembre et janvier inclus.

Trouver la boutique correspondante selon la météo des villes

Les ventes semble être corrélées à la météo. Dans ce contexte pour déterminer à quelle boutique le fichier ventes correspond, nous analyserons les données météo des boutiques situées à Bordeaux, Lille, Lyon, et Marseille.

Note: Afin d'intégrer une nouvelle ville à l'analyse, nous créerons une fonction automatisant la création d'un dataframe

Sélection des variables météorologiques

Découverte des variables

In [9]:
bordeaux.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 365 entries, 0 to 364
Data columns (total 22 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   DATE                    365 non-null    object 
 1   MAX_TEMPERATURE_C       365 non-null    int64  
 2   MIN_TEMPERATURE_C       365 non-null    int64  
 3   WINDSPEED_MAX_KMH       365 non-null    int64  
 4   TEMPERATURE_MORNING_C   365 non-null    int64  
 5   TEMPERATURE_NOON_C      365 non-null    int64  
 6   TEMPERATURE_EVENING_C   365 non-null    int64  
 7   PRECIP_TOTAL_DAY_MM     365 non-null    float64
 8   HUMIDITY_MAX_PERCENT    365 non-null    int64  
 9   VISIBILITY_AVG_KM       365 non-null    float64
 10  PRESSURE_MAX_MB         365 non-null    int64  
 11  CLOUDCOVER_AVG_PERCENT  365 non-null    float64
 12  HEATINDEX_MAX_C         365 non-null    int64  
 13  DEWPOINT_MAX_C          365 non-null    int64  
 14  WINDTEMP_MAX_C          365 non-null    int64  
 15  WEATHER_CODE_MORNING    365 non-null    int64  
 16  WEATHER_CODE_NOON       365 non-null    int64  
 17  WEATHER_CODE_EVENING    365 non-null    int64  
 18  TOTAL_SNOW_MM           365 non-null    int64  
 19  UV_INDEX                365 non-null    int64  
 20  SUNHOUR                 365 non-null    float64
 21  OPINION                 365 non-null    object 
dtypes: float64(4), int64(16), object(2)
memory usage: 62.9+ KB

Corrélation des variables

Nous créerons une fonction qui nous permettra d'afficher une heatmap de la matrice des corrélations.

La fonction corr() de Pandas sera utilisé pour trouver la corrélation par paire de toutes les colonnes de la trame de données. Nous rajouterons un argument method pour utiliser la méthode de Spearman lorsque les données ne seront pas normalisées.

Nous créerons un mask avec zeros_like() de NumPy pour renvoyer un tableau de formes et de tailles similaires avec les valeurs des éléments du tableau remplacées par des zéros. Dans ce même package, nous utiliserons aussi la fonction triu_indices_from() pour obtenir une copie des éléments d'un tableau.

Pour produire l'heatmap nous utiliserons le package Seaborn possédant dans ses arguments la possibilité d'inclure notre mask.

Nous concaténerons les données météo pour chaque ville dans un même dataframe: meteo.

Dans un nouveau DataFrame data, nous joindrons les deux dataframes ventes et meteo sur la colonne DATE afin de vérifier quelles sont les variables météorologiques les plus corrélées aux ventes d'ITEM.

Fonction display_matrice_corr()

In [10]:
def display_matrice_corr(df, method="pearson"):
    """
    Fonction d'analyse visuelle qui illustre la corrélation par
    paire de toutes les colonnes de la trame de données.
    
    Dependencies:
    - seaborn
    - matplotlib
    
    Arguments :
        df (DataFrame): Dataframe contenant les variables à corréler.
        method (str) : Méthode utilisée pour calculer les corrélations. Les options sont "pearson" (par défaut) et "spearman".
        
    Returns:
        Heatmap de la matrice des corrélations.
    """
    # Calcul de la matrice de corrélation
    if method == "spearman":
        corr = df.corr(method="spearman")
    else:
        corr = df.corr()
    # Création du mask
    mask = np.zeros_like(corr)
    triangle_indices = np.triu_indices_from(mask)
    mask[triangle_indices] = True

    # Visualisation de la matrice en heatmap
    with sns.axes_style("white"):
        plt.figure(figsize=(16, 10))
        sns.heatmap(corr.round(2),
                    mask=mask,
                    annot=True,
                    annot_kws={"size": 13},
                    cmap="RdYlBu",
                    linewidths=1)
        plt.xticks(fontsize=14, rotation=90)
        plt.yticks(fontsize=14, rotation=0)
        plt.title("Matrice de corrélation", size=24)

Création de data

In [11]:
# On concatène les données météo pour chaque ville dans un même dataframe
meteo = pd.concat([bordeaux, lille, lyon, marseille], ignore_index=True)

# On converti la colonne "DATE" en format datetime pour les deux dataframes
ventes["DATE"] = pd.to_datetime(ventes["DATE"])
meteo["DATE"] = pd.to_datetime(meteo["DATE"])

# On joint les deux dataframes sur la colonne "DATE"
data = pd.merge(ventes, meteo, on="DATE", how="inner")

Heatmap de la matrice de corrélation

In [12]:
# Nos données ne sont pas normalisées, nous utiliserons la méthode de Spearman
display_matrice_corr(data, method="spearman")

Résultat

Les coefficients de corrélation renvoient la force et le sens de la relation linéaire entre deux variables. Un coefficient de corrélation de 1 indique une relation parfaitement positive linéaire, c'est-à-dire que les variables augmentent ensemble.
Un coefficient de corrélation de -1 indique une relation parfaitement négative linéaire, c'est-à-dire que l'une des variables augmente tandis que l'autre diminue. Un coefficient de corrélation de 0 indique que les variables ne sont pas liées.

D'après les résultats obtenus, il y a une très faible corrélation positive entre la température maximale (TEMPERATURE_MAX) et les ventes (coefficient de corrélation de 0.07).
Il n'y a pas de corrélation significative entre la vitesse maximale du vent (WINDSPEED_MAX_KMH) et les ventes (coefficient de corrélation de -0.04) ou entre les précipitations totales (PRECIP_TOTAL_DAY_MM) et les ventes (coefficient de corrélation de -0.04).
Il y a une faible corrélation positive entre la température du vent (WINDTEMP_MAX_C) et les ventes (coefficient de corrélation de 0.07) et pas de corrélation significative entre la couverture nuageuse (CLOUDCOVER_AVG_PERCENT) et les ventes (coefficient de corrélation de -0.04).

Note: ces résultats ne prouvent pas une causalité entre les variables, mais seulement une corrélation. Il est possible qu'il y ait d'autres facteurs qui influencent les ventes et qu'il faut prendre en compte pour une analyse plus complète.

Impact de la météo sur ventes d'ITEM

Même si sur l'ensemble des ventes une faible corrélation existe entre certaines données météorologiques, il serait intéressant de vérifier si une corrélation semble plus forte selon le produit vendu.

Pour rappel, lors de l'analyse de la saisonnalité des ventes (Voir chapître 4.2 Saisonnalité des ventes), nous avions pu observer un net écart entre les mois de mai et octobre (tous deux inclus) où le CA des produits B est plus fort. A l'inverse le CA des produits A est plus fort entre novembre et janvier inclus.

Grâce à la fonction merge() de Pandas, nous créerons une fonction qui fusionnera les données de ventes et de météo sur la colonne DATA.
Avec la libraire scipy.stats nous calculerons le coefficielent de corrélation entre les ventes générales, ainsi que les ventes par ITEM, avec les variables météorologiques de température maximale, vitesse maximale du vent et précipitations totales et renvoie un tableau des corrélations

In [13]:
def correlation_ventes_meteo(ventes, meteo, method="pearson"):
    """
    Cette fonction prend en entrée un dataframe "ventes" contenant les données
    de ventes et un dataframe "meteo" contenant les données météorologiques.
    Elle calcule les coefficients de corrélation entre les ventes et les
    variables météorologiques de température maximale, vitesse maximale du
    vent et précipitations totales et renvoie un tableau des corrélations.
    
    Dependencies:
    - pandas
    - scipy.stats
    
    Parameters:
        ventes (DataFrame): Dataframe contenant les données de ventes.
        meteo (DataFrame): Dataframe contenant les données météorologiques.
        
    Returns:
        Dataframe contenant les coefficients de corrélation.
    """
    # On fusionne les données ventes et météo sur la colonne "DATE"
    data = pd.merge(ventes, meteo, on="DATE")
    # On crée un dictionnaire pour stocker les coefficients de corrélation
    corr = {}
    # Calcul de la matrice de corrélation
    if method == "spearman":
        corr_func = stats.spearmanr
    else:
        corr_func = stats.pearsonr
    # On calcule les coefficients de corrélation entre les ventes
    # et les variables météorologiques
    corr["TEMPERATURE_MAX"] = corr_func(data["SALES"],
                                        data["MAX_TEMPERATURE_C"])[0]
    corr["VITESSE_MAX"] = corr_func(data["SALES"],
                                    data["WINDSPEED_MAX_KMH"])[0]
    corr["COUVERTURE_NUAGEUSE"] = corr_func(data["SALES"],
                                            data["CLOUDCOVER_AVG_PERCENT"])[0]
    items = data['ITEM'].unique()
    for item in items:
        corr[f"TEMPERATURE_MAX_{item}"] = corr_func(
            data[data["ITEM"] == item]["SALES"],
            data[data["ITEM"] == item]["MAX_TEMPERATURE_C"])[0]
        corr[f"VITESSE_MAX_{item}"] = corr_func(
            data[data["ITEM"] == item]["SALES"],
            data[data["ITEM"] == item]["WINDSPEED_MAX_KMH"])[0]
        corr[f"COUVERTURE_NUAGEUSE_{item}"] = corr_func(
            data[data["ITEM"] == item]["SALES"],
            data[data["ITEM"] == item]["CLOUDCOVER_AVG_PERCENT"])[0]
    # On crée un tableau des corrélations
    corr_table = pd.DataFrame.from_dict(corr,
                                        orient="index",
                                        columns=["Coefficient de corrélation"])

    # Calcul de la matrice de corrélation pour toutes les colonnes du dataframe
    corr = data.corr()

    return corr_table.round(2)

Création de la fonction plot_evo_mensuel_meteo()

Nous créerons une fonction pour vérifier l'évolution des données météos pour la ville donnée selon les variables les plus corrélées au fichier ventes.

Dans un premier temps, pour coller avec la saisonnalité de notre fichier ventes, nous agrégerons la variable DATE selon le mois.

Afin de faciliter l'analyse météorologique, nous pouvons limiter le nombre de variable à 4:

  • le mois;
  • la température max moyenne (MAX_TEMPERATURE_C);
  • la vitesse max moyenne du vent (WINDSPEED_MAX_KMH);
  • la couverture nuageuse moyenne (CLOUDCOVER_AVG_PERCENT).
In [14]:
def plot_evo_mensuel_meteo(df, ville):
    """
    Cette fonction prend en entrée un dataframe 'df' et un nom de ville
    'ville' et renvoie un graphique représentant l'évolution mensuelle des 
    données météorologiques pour la ville donnée. Les données météorologiques
    étudiées sont: la température maximale en degré celsius, la vitesse
    maximale du vent en km/h et la couverture nuageuse moyenne.
    
    Dependencies:
    - pandas
    - plotly
    
    Parameters:
    df (DataFrame): Dataframe contenant les données météorologiques.
    ville (str): Nom de la ville pour laquelle les données météorologiques
    sont étudiées.
    
    Returns:
    Graphique de l'évolution mensuelle des données météorologiques pour
    la ville donnée.
    """
    # On convertit la colonne "DATE" en format date
    # errors="coerce" permet de forcer la conversion des valeurs en NaT
    df["DATE"] = pd.to_datetime(df["DATE"], errors="coerce")
    # On supprime les lignes contenant des valeurs manquantes pour "DATE"
    df = df.dropna(subset=["DATE"])
    # On crée une nouvelle colonne 'MOIS' pour regrouper les données
    # météorologiques par mois
    df["MOIS"] = df["DATE"].dt.strftime("%m")
    # On groupe les données météorologiques par mois pour la ville donnée
    result = df.groupby("MOIS")[[
        "MAX_TEMPERATURE_C", "WINDSPEED_MAX_KMH", "CLOUDCOVER_AVG_PERCENT"
    ]].mean().round(2)

    # On crée le graphique de l'évolution mensuelle des données météorologiques
    fig = go.Figure()
    fig.add_trace(
        go.Scatter(x=result.index,
                   y=result["MAX_TEMPERATURE_C"],
                   mode="lines+markers",
                   name="Température max en C°"))
    fig.add_trace(
        go.Scatter(x=result.index,
                   y=result["WINDSPEED_MAX_KMH"],
                   mode="lines+markers",
                   name="Vitesse max en KM/H"))
    fig.add_trace(
        go.Scatter(x=result.index,
                   y=result["CLOUDCOVER_AVG_PERCENT"],
                   mode="lines+markers",
                   name="Couverture nuageuse moyenne"))

    fig.update_layout(
        xaxis_title="Mois",
        yaxis_title="Mesures météorologiques en moyennes",
        title=
        f"<b>Evolution mensuelle des données météorologiques pour la ville de {ville}</b>"
    )

    fig.show()

Visualisation de l'évolution mensuelle des données météorologiques...

... pour la ville de Bordeaux

In [15]:
plot_evo_mensuel_meteo(bordeaux, "Bordeaux")
In [16]:
correlation_ventes_meteo(ventes, bordeaux, method="spearman")
Out[16]:
Coefficient de corrélation
TEMPERATURE_MAX 0.08
VITESSE_MAX -0.04
COUVERTURE_NUAGEUSE -0.06
TEMPERATURE_MAX_A -0.86
VITESSE_MAX_A 0.37
COUVERTURE_NUAGEUSE_A 0.71
TEMPERATURE_MAX_B 0.88
VITESSE_MAX_B -0.37
COUVERTURE_NUAGEUSE_B -0.69

... pour la ville de Lille

In [17]:
plot_evo_mensuel_meteo(lille, "Lille")
In [18]:
correlation_ventes_meteo(ventes, lille, method="spearman")
Out[18]:
Coefficient de corrélation
TEMPERATURE_MAX 0.07
VITESSE_MAX -0.02
COUVERTURE_NUAGEUSE -0.04
TEMPERATURE_MAX_A -0.72
VITESSE_MAX_A 0.18
COUVERTURE_NUAGEUSE_A 0.44
TEMPERATURE_MAX_B 0.75
VITESSE_MAX_B -0.17
COUVERTURE_NUAGEUSE_B -0.44

... pour la ville de Lyon

In [19]:
plot_evo_mensuel_meteo(lyon, "Lyon")
In [20]:
correlation_ventes_meteo(ventes, lyon, method="spearman")
Out[20]:
Coefficient de corrélation
TEMPERATURE_MAX 0.07
VITESSE_MAX -0.01
COUVERTURE_NUAGEUSE -0.04
TEMPERATURE_MAX_A -0.75
VITESSE_MAX_A 0.24
COUVERTURE_NUAGEUSE_A 0.62
TEMPERATURE_MAX_B 0.78
VITESSE_MAX_B -0.21
COUVERTURE_NUAGEUSE_B -0.59

... pour la ville de Marseille

In [21]:
plot_evo_mensuel_meteo(marseille, "Marseille")
In [22]:
correlation_ventes_meteo(ventes, marseille, method="spearman")
Out[22]:
Coefficient de corrélation
TEMPERATURE_MAX 0.07
VITESSE_MAX -0.03
COUVERTURE_NUAGEUSE -0.04
TEMPERATURE_MAX_A -0.72
VITESSE_MAX_A 0.37
COUVERTURE_NUAGEUSE_A 0.53
TEMPERATURE_MAX_B 0.75
VITESSE_MAX_B -0.36
COUVERTURE_NUAGEUSE_B -0.51

Résultat concernant la boutique

Il semble que la ville avec les données météorologiques les plus corrélées aux ventes pour les items A et B soit Bordeaux.
En effet, la corrélation entre les ventes et les variables météorologiques pour cette ville est plus élevée que pour les autres villes.
Cependant, il est important de noter que ces coefficients de corrélation sont faibles et ne permettent pas de tirer des conclusions définitives sur les relations entre les données météorologiques et les ventes.

Note: ces résultats ne prouvent pas une causalité entre les variables, mais seulement une corrélation. Il est possible qu'il y ait d'autres facteurs qui influencent les ventes et qu'il faut prendre en compte pour une analyse plus complète.

Résultat concernant la variable météo la plus corrélée

In [23]:
# On concatène les données météo pour chaque ville dans un même dataframe
meteo = pd.concat([bordeaux, lille, lyon, marseille], ignore_index=True)

correlation_ventes_meteo(ventes, meteo, method="spearman")
Out[23]:
Coefficient de corrélation
TEMPERATURE_MAX 0.07
VITESSE_MAX -0.02
COUVERTURE_NUAGEUSE -0.04
TEMPERATURE_MAX_A -0.74
VITESSE_MAX_A 0.26
COUVERTURE_NUAGEUSE_A 0.54
TEMPERATURE_MAX_B 0.77
VITESSE_MAX_B -0.25
COUVERTURE_NUAGEUSE_B -0.53
In [24]:
plot_saisonnalite_ventes(ventes, viz="line")

La température semble être la variable météorologique la plus impactante concernant les ventes.

D'après les résultats obtenus:

  • il y a une très forte corrélation négative entre la température maximale (TEMPERATURE_MAX) et les ventes des items A (coefficient de corrélation de -0.74) puisque les ventes semblent baisser lorsque la température augmente.
    A l'inverse, une très forte corrélation positive s'observe avec les ventes des items B (coefficient de corrélation de 0.77);
  • il y a une forte corrélation positive entre la couverture nuageuse moyenne (CLOUDCOVER_AVG_PERCENT) et les ventes des items A (coefficient de corrélation de 0.54) puisque les ventes semblent augmenter lorsque la couverture nuageuse augmente.
    A l'inverse, une forte corrélation négative s'observe avec les ventes des items B (coefficient de corrélation de -0.53).

Note: ces résultats ne prouvent pas une causalité entre les variables, mais seulement une corrélation. Il est possible qu'il y ait d'autres facteurs qui influencent les ventes et qu'il faut prendre en compte pour une analyse plus complète.

Prévision des ventes du 21 au 27 juin

Vérification des données

In [25]:
prev_meteo_fin_juin.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7 entries, 0 to 6
Data columns (total 9 columns):
 #   Column                  Non-Null Count  Dtype  
---  ------                  --------------  -----  
 0   DATE                    7 non-null      object 
 1   MAX_TEMPERATURE_C       7 non-null      int64  
 2   MIN_TEMPERATURE_C       7 non-null      int64  
 3   WINDSPEED_MAX_KMH       7 non-null      int64  
 4   PRECIP_TOTAL_DAY_MM     7 non-null      float64
 5   HUMIDITY_MAX_PERCENT    7 non-null      int64  
 6   VISIBILITY_AVG_KM       7 non-null      float64
 7   PRESSURE_MAX_MB         7 non-null      int64  
 8   CLOUDCOVER_AVG_PERCENT  7 non-null      float64
dtypes: float64(3), int64(5), object(1)
memory usage: 632.0+ bytes

Pour réaliser notre prédiction, nous utiliserons une régression linéaire multiple avec une méthode pas-à-pas descendant (stepwise). Cette procédure itérative nous permettra d'optimiser nos modèles en nous donnant la possibilité d'éliminer les variables devenues non significatives.
Nous trouverons le modèle le plus parcimonieux (en minimisant AIC) avec un ($R^{2}$) et un F-stat important.

Nous diviserons le process en différentes étapes:

  • la préparation du dataset de prédiction, nous devrons merger les dataframes ventes et bordeaux (qui correspond à notre boutique, voir chapître 5.5 Résultat concernant la boutique) en nous basant sur la variable DATE.
    Nous n'utiliserons que les variables communes avec le dataframe prev_meteo_fin_juin. Pour cela nous utiliserons la méthode intersection(), nous copierons le résultat dans un nouveau dataframe ventes_bordeaux_predict. La temporalité n'étant pas la même entre prev_meteo_fin_juin et ventes_bordeaux, nous supprimerons la variable DATE;

  • le feature scaling des données, nos données météorologiques ayant des unités différentes de mesures entre elles il conviendra de les centrer-réduire. Le principal avantage de la centration-réduction est de rendre comparables des variables qui ne le seraient pas directement parce qu'elles ont des moyennes et ou des variances trop différentes. Les fonctions de feature scaling sont regroupées dans le package preprocessing de Sklearn;

  • puisque l'impact des données météos changent selon l'item nous séparerons notre jeu de donnée par item. Nous utiliserons la fonction *loc() de Pandas;

Création de ventes_bordeaux_predict

In [26]:
# On réalise notre jointure
ventes_bordeaux = pd.merge(ventes, bordeaux, on="DATE", how="left")

# On sélectionne les variables en commun entre les deux dataframes en
# excluant la colonne "DATE"
common_vars = list(
    set(ventes_bordeaux.columns).intersection(
        prev_meteo_fin_juin.columns.difference(["DATE"])))

# On conserve seulement les variables en commun et la colonne "ITEM"
# dans ventes_bordeaux
ventes_bordeaux_predict = ventes_bordeaux[["SALES", "ITEM"] + common_vars]

Feature scaling des données

Nous allons normaliser ici uniquement les variables météorologiques, nous n'incluerons donc pas DATE et ITEM dans la trasnformation des données.
Afin d'améliorer les performances de notre algorithme de prédiction nous traiterons chaque variable de manière optimale. Pour cela nous créerons une fonction qui utilisera l'une des méthodes de sklearn.preprocessing comme MinMaxScaler, StandardScaler ou RobustScaler.

Création de la fonction normalize_ventes_meteo()

In [27]:
def normalize_ventes_meteo(df):
    """
    Cette fonction prend en entrée un dataframe (df) et retourne ce même
    dataframe après avoir normalisé toutes les colonnes sauf "SALES" et "ITEM".
    La méthode de normalisation utilisée dépend de la distribution des
    données de chaque colonne:
        - Si la colonne a une forte asymétrie (skewness > 0.75),
            la méthode utilisée est RobustScaler
        - Si la colonne a une forte Kurtosis (kurtosis > 3),
            la méthode utilisée est StandardScaler
        - Sinon, la méthode utilisée est MinMaxScaler
        
    Dependencies:
        - pandas
        - sklearn.preprocessing (MinMaxScaler, StandardScaler, RobustScaler)
            
    Parameters:
        - df (Dataframe): Dataframe contenant les variables météo à normaliser.
        
    Returns:
        - df (Dataframe): Dataframe contenant les variables météo normalisées. 
    """
    columns_to_normalize = [
        col for col in df.columns if col not in ["SALES", "ITEM"]
    ]
    for col in columns_to_normalize:
        if df[col].skew() > 0.75:
            scaler = RobustScaler()
        elif df[col].kurtosis() > 3:
            scaler = StandardScaler()
        else:
            scaler = MinMaxScaler()
        df.loc[:, col] = scaler.fit_transform(df[[col]])
    return df

Jeu de donnée normalisé

In [28]:
ventes_bordeaux_predict_normalize = normalize_ventes_meteo(
    ventes_bordeaux_predict)

Séparation du jeu de donnée selon l'item vendu

In [29]:
# Uniquement les items A
ventes_bordeaux_predict_normalize_A = ventes_bordeaux_predict_normalize.loc[
    ventes_bordeaux_predict_normalize["ITEM"] == "A"]
# Uniquement les items B
ventes_bordeaux_predict_normalize_B = ventes_bordeaux_predict_normalize.loc[
    ventes_bordeaux_predict_normalize["ITEM"] == "B"]

Test d'algorithmes

Nous testerons ici sur nos deux jeux de données ventes_bordeaux_predict_normalize_A et ventes_bordeaux_predict_normalize_B quel algorithme nous permettrait d'obtenir la meilleure prédiction.
Nous créerons une fonction pour tester 4 algorithmes:

  • LinearRegression, est un modèle de régression linéaire simple qui prédit la variable cible en utilisant une combinaison linéaire des variables indépendantes. Il permet d'estimer les coefficients de régression et de prévoir les valeurs de la variable cible;

  • KNeighborsRegressor, est un modèle de régression basé sur l'algorithme des k plus proches voisins. Il prédit la valeur cible en utilisant les valeurs cibles des k observations les plus proches de la nouvelle observation. Il est souvent utilisé pour des tâches de regression non linéaires;

  • SVR, est un modèle de régression basé sur les machines à vecteurs de support. Il est utilisé pour résoudre des problèmes de régression dans lesquels les relations entre les variables indépendantes et dépendantes sont non linéaires. Il peut être utilisé avec différents types de noyaux pour résoudre des problèmes de régression non linéaires;

  • RandomForestRegressor, est un modèle de régression basé sur l'algorithme de forêt aléatoire. Il construit plusieurs arbres de décision et utilise la moyenne des prédictions de ces arbres pour prévoir la variable cible. Il est souvent utilisé pour des tâches de régression non linéaires et peut également être utilisé pour sélectionner des variables importantes.
    Pour interpréter ces résultats nous utiliserons deux indicateurs:

  • MSE, le Mean Squared Error (MSE) est une mesure de la qualité de la prédiction d'un modèle de régression. Il mesure l'écart quadratique moyen entre les valeurs réelles et les valeurs prédites. Plus le MSE est faible, meilleure est la performance de prédiction du modèle;
  • $R^{2}$, (ou coefficient de détermination) est un indicateur de la qualité de la prédiction d'un modèle de régression. Il est défini comme la proportion de la variance totale des données expliquée par le modèle. Plus le $R^{2}$ est proche de 1, plus le modèle est efficace pour expliquer les variations des données d'entraînement. Un $R^{2}$ de 0 indique que le modèle ne peut pas expliquer les variations des données et un $R^{2}$ de -1 indique que les prédictions sont pires que des prédictions aléatoires.

Création de la fonction test_algorithms()

In [30]:
def test_algorithms(df):
    """
    Cette fonction prend en entrée un dataframe (df) et teste différents
    modèles de régression sur les données contenues dans ce dataframe. Les
    modèles utilisés sont Linear Regression, KNN, SVM et Random Forest. Les
    données sont divisées en données d'entraînement et de test avant d'être
    utilisées. Les résultats de chaque modèle sont affichés sous la forme d'un
    score MSE et d'un score R2.
    
    Dependencies:
        - pandas
        - sklearn.model_selection (train_test_split)
        - sklearn.metrics (mean_squared_error, r2_score)
        - sklearn.linear_model (LinearRegression)
        - sklearn.neighbors (KNeighborsRegressor)
        - sklearn.svm (SVR)
        - sklearn.ensemble (RandomForestRegressor)
    
    Parameters:
        - dataframe (Dataframe): Dataframe contenant les données à utiliser pour
                                entraîner les modèles.
    Returns:
        - None. Affiche les résultats de l'évaluation des différents modèles de 
        régression: mean squared error (MSE) et coefficient de détermination (R2)
        pour chaque modèle. 
    """
    # On sélectionne les variables météo
    meteo_vars = [
        "MAX_TEMPERATURE_C", "WINDSPEED_MAX_KMH", "CLOUDCOVER_AVG_PERCENT",
        "HUMIDITY_MAX_PERCENT", "MIN_TEMPERATURE_C", "PRECIP_TOTAL_DAY_MM",
        "VISIBILITY_AVG_KM", "PRESSURE_MAX_MB"
    ]

    # On sépare les données en données d'entraînement et de test
    X_train, X_test, y_train, y_test = train_test_split(df[meteo_vars],
                                                        df["SALES"],
                                                        test_size=0.2)

    # On entraîne différents modèles de régression
    models = {
        'Linear Regression': LinearRegression(),
        'KNN': KNeighborsRegressor(),
        'SVM': SVR(),
        'Random Forest': RandomForestRegressor()
    }

    # On évalue chaque modèle sur les données de test
    for name, model in models.items():
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        print(
            f"{name} MSE: {mean_squared_error(y_test, y_pred).round(2)} R2: {r2_score(y_test, y_pred).round(2)}"
        )

Test sur ventes_bordeaux_predict_normalize_A

In [31]:
test_algorithms(ventes_bordeaux_predict_normalize_A)
Linear Regression MSE: 360.68 R2: 0.8
KNN MSE: 199.79 R2: 0.89
SVM MSE: 1130.27 R2: 0.37
Random Forest MSE: 22.86 R2: 0.99

Test sur ventes_bordeaux_predict_normalize_B

In [32]:
test_algorithms(ventes_bordeaux_predict_normalize_B)
Linear Regression MSE: 229.69 R2: 0.94
KNN MSE: 663.1 R2: 0.83
SVM MSE: 2762.49 R2: 0.3
Random Forest MSE: 182.46 R2: 0.95

Résultat

Le Random Forest a un $R^{2}$ proche de 1 pour les deux jeux de données, (0.93 pour les items A et 0.94 pour les items B) ce qui indique que les prédictions sont très proches des valeurs réelles. Il est donc un bon choix pour prédire les ventes en fonction des variables météo. De plus leur MSE est aussi le plus proche de 0 (9.71 pour les items A et 239.88 pour les items B) nous donnant la meilleure performance de prédiction.

Prédiction avec Random Forest

Etiquettage des variables

In [33]:
# Variable dépendante pour les items A
y_a = ventes_bordeaux_predict_normalize_A["SALES"]
# Variable dépendante pour les items B
y_b = ventes_bordeaux_predict_normalize_B["SALES"]

# Variables météos
meteo_vars = [
    "MAX_TEMPERATURE_C", "WINDSPEED_MAX_KMH", "CLOUDCOVER_AVG_PERCENT",
    "HUMIDITY_MAX_PERCENT", "MIN_TEMPERATURE_C", "PRECIP_TOTAL_DAY_MM",
    "VISIBILITY_AVG_KM", "PRESSURE_MAX_MB"
]

# Variables explicatives quantitatives pour les items A
X_a = ventes_bordeaux_predict_normalize_A[meteo_vars]
# Variables explicatives quantitatives pour les items A
X_b = ventes_bordeaux_predict_normalize_B[meteo_vars]

Entraînement et test

In [34]:
# Pour les items A
X_a_train, X_a_test, y_a_train, y_a_test = train_test_split(X_a,
                                                            y_a,
                                                            train_size=0.8,
                                                            random_state=42)
# Pour les items B
X_b_train, X_b_test, y_b_train, y_b_test = train_test_split(X_b,
                                                            y_b,
                                                            train_size=0.8,
                                                            random_state=42)

Fonction create_random_forest_model()

In [35]:
def create_random_forest_model(X_train, y_train):
    """
    Cette fonction prend en entrée des données d'entraînement (X_train, y_train)
    et retourne un modèle de prédiction utilisant l'algorithme RandomForestRegressor.
    
    Dependencies:
        - sklearn.ensemble (RandomForestRegressor)
    
    Parameters:
        - X_train (DataFrame): Dataframe contenant les variables explicatives
                                pour les données d'entraînement
        - y_train (Series): Series contenant les variables dépendantes pour
                                les données d'entraînement
    
    Returns:
        - model (RandomForestRegressor): modèle de prédiction utilisant 
                                            l'algorithme RandomForestRegressor
    """
    model = RandomForestRegressor(random_state=42)
    model.fit(X_train, y_train)
    return model

Création des modèles de prédiction

In [36]:
# Pour les item A
model_a = create_random_forest_model(X_a_train, y_a_train)
# Pour les item
model_b = create_random_forest_model(X_b_train, y_b_train)

Vérification de la performance des modèles

In [37]:
# Prédictions pour les items A
y_a_pred = model_a.predict(X_a_test)
# Prédictions pour les items B
y_b_pred = model_b.predict(X_b_test)

# Évaluation du modèle pour les items A
print(
    f"Item A - MSE : {mean_squared_error(y_a_test, y_a_pred).round(2)}, R2 : {r2_score(y_a_test, y_a_pred).round(2)}"
)
# Évaluation du modèle pour les items B
print(
    f"Item B - MSE : {mean_squared_error(y_b_test, y_b_pred).round(2)}, R2 : {r2_score(y_b_test, y_b_pred).round(2)}"
)
Item A - MSE : 58.4, R2 : 0.98
Item B - MSE : 170.85, R2 : 0.96

A la vue des résultats notre modèle de prédiction semble être très précis.

Résultat des stocks à prévoir

In [38]:
prev_meteo_fin_juin["STOCK_A"] = model_a.predict(
    normalize_ventes_meteo(prev_meteo_fin_juin[meteo_vars])).round()
prev_meteo_fin_juin["STOCK_B"] = model_b.predict(
    normalize_ventes_meteo(prev_meteo_fin_juin[meteo_vars])).round()
prev_meteo_fin_juin
Out[38]:
DATE MAX_TEMPERATURE_C MIN_TEMPERATURE_C WINDSPEED_MAX_KMH PRECIP_TOTAL_DAY_MM HUMIDITY_MAX_PERCENT VISIBILITY_AVG_KM PRESSURE_MAX_MB CLOUDCOVER_AVG_PERCENT STOCK_A STOCK_B
0 2021-06-21 20 14 23 2.0 92 7.0 1025 61.0 164.0 18.0
1 2021-06-22 25 17 10 0.0 73 9.0 1027 26.0 51.0 137.0
2 2021-06-23 25 18 19 0.0 79 10.0 1024 19.0 51.0 137.0
3 2021-06-24 32 23 12 0.0 76 10.0 1018 2.0 31.0 260.0
4 2021-06-25 28 20 13 1.0 82 10.0 1015 36.0 70.0 187.0
5 2021-06-26 20 16 19 0.0 88 9.0 1013 89.0 178.0 42.0
6 2021-06-27 21 15 31 1.0 89 9.0 1015 76.0 152.0 33.0

Graphique de la prévision des stocks et des données météos

Nous comparerons ici l'évolution des prévisions météorologiques de la semaine du 21 au 27 juin avec les stocks à prévoir pour les items A et B.

Création de la fonction plot_stock_evolution()

In [39]:
def plot_stock_evolution(df, meteo_vars):
    """
    Cette fonction permet de visualiser l'évolution des stocks
    A et B en fonction de la date et de plusieurs variables
    météorologiques. Il crée un sous-graphe pour chaque variable
    météo, ajoute les tracés pour chaque variable météo et pour
    les stocks A et B, met à jour les titres des axes x et y et
    affiche le graphe.
    
    Dependencies:
        - Plotly.graph_objs
        - Plotly.subplots
    
    Parameters:
        - df (DataFrame): Le DataFrame contenant les données à
                            visualiser
        - meteo_vars (list): La liste des variables météorologiques
                            à inclure dans le graphe

    Returns:
        Subplot de l'évolution des stocks A et B et des donnés météos
    """
    # On crée un sous-graphe pour chaque variable météo
    fig = make_subplots(rows=len(meteo_vars) + 1,
                        cols=1,
                        shared_xaxes=True,
                        vertical_spacing=0.1)
    # On ajoute les tracés pour chaque variable météo
    for i, var in enumerate(meteo_vars):
        fig.add_trace(go.Scatter(x=df["DATE"],
                                 y=df[var],
                                 name=var,
                                 mode="lines+markers"),
                      row=i + 1,
                      col=1)
    # On ajoute les tracés pour STOCK_A et STOCK_B
    fig.add_trace(go.Scatter(x=df['DATE'],
                             y=df['STOCK_A'],
                             name='STOCK_A',
                             mode='lines+markers'),
                  row=len(meteo_vars) + 1,
                  col=1)
    fig.add_trace(go.Scatter(x=df['DATE'],
                             y=df['STOCK_B'],
                             name='STOCK_B',
                             mode='lines+markers'),
                  row=len(meteo_vars) + 1,
                  col=1)
    # On met à jour les titres de l'axe x et y
    fig.update_layout(
        title=f"<b>Prévision des stocks et des données météos</b>")
    # On affiche le graphe
    fig.show()

Visualisation

In [40]:
# Variables météos
meteo_vars = ["MAX_TEMPERATURE_C", "CLOUDCOVER_AVG_PERCENT"]

plot_stock_evolution(prev_meteo_fin_juin, meteo_vars)

On observe bien une corrélation entre la hausse des températures et l'augmentation du stock de produits B, à l'inverse lorsque la couverture nuageuse arrive, annonçant la pluie, les stock des produits A augmente.